(ns
  #^{:doc 
   "This is the Site management API for a molehill.
   
   A molehill is composed of
    1. entries (called hill-files),
    2. a VCS system (for tracking changes to the site)
    3. Templates (defining structure for a sites pages)
   
   There are two kinds of pages for a molehill.
    1. Entry pages - These contain only one entry with no aggregate
      content. They can be updated as they are encountered in a site build.
    2. Aggregate pages - These contain multiple aggregates.
      They have to wait until all indexes have been updated in order
      to assure that all changed entries will be updated in the page
      and avoid having to rewrite them multiple times.
   
   Pages are built from the content of a set of hill-files. Essentially
   a molehill is a gigantic database of text content with a way to
   present that content for consumption."}
  com.marzhillstudios.molehill.site
  (:require
     [com.marzhillstudios.molehill.vcs :as vcs]
     [com.marzhillstudios.molehill.indexer :as i]
     [com.marzhillstudios.molehill.import-wordpress :as iw]
     [com.marzhillstudios.molehill.http-server :as hs]
     [clojure.main :as cm]
     [clojure.contrib.math :as m])
  (:use [clojure.contrib.set :only [subset?]]
        [clojure.contrib.except :only [throw-arg]]
        [clojure.contrib.seq-utils :only [flatten]]
        [clojure.contrib.duck-streams :only [file-str with-out-writer copy]]
        [clojure.contrib.logging]
        [com.marzhillstudios.list.util :only [lazy-cons*]]
        [com.marzhillstudios.path]
        [com.marzhillstudios.molehill.log-util]
        [com.marzhillstudios.molehill.utils]
        [com.marzhillstudios.molehill.file :only [hill-file<-disk
                                                  hill-file->disk
                                                  hill-file-slug
                                                  new-hill-file
                                                  files-from-list]]
        [com.marzhillstudios.molehill.template :only [mk-template-state
                                                      call-template
                                                      with-templates
                                                      load-templates]]
        [com.marzhillstudios.molehill.config :only [defsite with-config
                                                    *site-config*]])
  (:gen-class))

; TODO(jwall): Draft content
; TODO(jwall): Content Filters?

(defn find-changed-files
  [] (let [file-map (vcs/changed-files)]
                  (set (flatten (vals file-map)))))

(defn all-content-files
  ([] (all-content-files *site-config*))
  ([config]
      (filter #(is-content-file? config %)
              (vcs/all-tracked))))
   
(defn vcs-add-if-not
  [file]
  (cond (not (vcs/tracked? file))
    (vcs/add file)))

(with-config sub-task-update-index-file
  [config file index-file]
  (let [index-dir (:site-index-dir config)
        index (str index-dir "/" index-file)]
    (info-fmt "updating index: %s" index)
    (i/update-index index
                    [(i/mk-update-request [(:epoch file) (dissoc file :parsed-content)]
                                          :index :update)])
    (vcs-add-if-not index)))

(defn tag-index-name
  [tag] (str "tag." tag ".clj"))

(with-config task-update-index
  [config file]
  (let [hill-file (hill-file<-disk (dirname file)
                                   (basename file))]
    (sub-task-update-index-file hill-file "feed.clj")
    (info-fmt "updating index for tags: %s" (:tags hill-file))
    (doseq [tag (:tags hill-file)]
      (sub-task-update-index-file hill-file (tag-index-name tag)))
    (sub-task-update-index-file hill-file "site.clj")))

(defn entry-file-name
  [file]
  (str (hill-file-slug file) "/index.html"))

(defn mk-directory
  [dir]
  (let [dir-file (file-str dir)]
    (cond (not (dir-exists? dir-file))
      (do
        (info-fmt "creating directory: %s" dir)
        (.mkdirs dir-file)))))

(with-config task-build-entry-pages
  [config file]
   (with-templates
    (let [template (:entry config)
          templated (call-template template file)
          tags (:tags file)
          root-dir (:site-root config)
          entry-dir (str root-dir "/entries/")
          filename (entry-file-name file)]
      ; write to the root/entries/filename/index.html location
      (info-fmt "writing entry %s to %s" (basename filename)
                (dirname (str entry-dir filename)))
      (mk-directory (dirname (str entry-dir filename))) 
      (with-out-writer (str entry-dir filename)
                       (print templated))
      ; TODO(jwall): rebuild the affected tag pages 
      ; TODO(jwall): rebuild the index and feed pages 
      ;(doseq [tag tags]
      ;  (let [tag-dir (str root-dir "/" tag "/")]
      ;    (info-fmt "writing entry %s to %s" (basename filename)
      ;              (dirname (str tag-dir filename)))
      ;    (info-fmt "writing entry to: %s/%s" tag-dir filename)
      ;    (with-out-writer (str tag-dir "/" filename)
      ;                     (print templated))))
      )))

(with-config tag-index-filename
  [config tag]
    (str (:site-index-dir config) "/tag." tag ".clj"))

(with-config get-all-tags
  [config]
   (debug-fmt "tag indexes: %s" (vcs/find-files (str (:site-index-dir config) "/tag.*.clj")))
    (for [tag (vcs/find-files (str (:site-index-dir config) "/tag.*.clj"))]
      (do (debug-fmt "first index: %s" tag)
      [(count (:index (i/get-index-from-file tag)))
       (last (re-find #".*/tag.(.*).clj" tag))])))

; returns [hill-file1,hill-file2,...]
(with-config get-all-for-tag
  [config tag]
    (debug-fmt "Getting all for tag: [%s]" tag)
    (i/files-from-index (:index (i/get-index-from-file
                              (tag-index-filename config tag)))))

; TODO(jwall): Unit Tests for the below functions
(defn map-tag-size
  "returns [[count tag1 size],[count tag2 size],...]"
  [lst]
  (cond (empty? lst) [[0 "default" 0]]
        (= (count lst) 1) [[1 (second (first lst)) 1]]
        :else
        (let [sizes (map first lst)
              mx (apply max 1 sizes)
              mn (apply min 1 sizes)
              ;avoid div by zero
              bucket (m/round (+ 1/2 (/ (- mx mn) (min mx 5))))]
          (map (fn [tag]
                 [(first tag)
                  (second tag)
                  (let [size (m/floor (/ (first tag) bucket))]
                    (cond (<= size 0) 1
                          :else size))])
               lst))))

; TODO(jwall): Unit Tests for the below functions
(with-config task-build-tag-page
  [config tag]
    (with-templates
      (info-fmt "Writing tag page for %s" tag)
      (mk-directory (str (:site-root config) "/" tag "/")) 
      (with-out-writer (file-str (:site-root config) "/" tag "/index.html")
                       (print (call-template (:tag-landing config) (get-all-for-tag tag))))))

(with-config task-build-tag-landing-page
  [config]
    (with-templates
      (info-fmt "Writing main tag page")
      (with-out-writer (file-str (:site-root config) "/tags.html")
                       (print (call-template (:tag config) (map-tag-size (get-all-tags)))))))

(with-config task-build-index-page
  [config index-entries]
    (with-templates
      (info-fmt "Writing index page")
      (with-out-writer (file-str (:site-root config) "/index.html")
                       (print (call-template (:index config) index-entries)))))

(with-config task-build-feed-page
  [config index-entries]
    (with-templates
      (info-fmt "Writing feed page")
      (with-out-writer (file-str (:site-root config) "/feed.rss")
                       (print (call-template (:feed config) index-entries)))))

(with-config task-build-aggregate-pages
  [config]
    (with-templates
      (let [feed-index (str (:site-index-dir config) "/feed.clj")
            site-index (str (:site-index-dir config) "/site.clj")
            index-entries (i/files-from-index (i/get-index-date-sorted site-index :index >))
            feed-entries (i/files-from-index (i/get-index-date-sorted feed-index :feed >))]
        ; build index page with all from site.clj index
        (task-build-index-page config index-entries)
        ; build feed page with all from feed.clj index
        (task-build-feed-page config feed-entries)
        (task-build-tag-landing-page config)
        (doseq [[_ tag] (get-all-tags)]
          (task-build-tag-page config tag)))))

; TODO(jwall): should this farm out the work to the various modules responsible?
; TODO(jwall): split up into smaller more testable functions
; TODO(jwall): what about vcs for files in _tmpl,
;     config file, or static content in generated?
(with-config task-site-init
  [config]
  ; init the vcs if it's necessary
   (cond
     (not (dir-exists? (file-str ".hg")))
       (do (info-fmt "initializing vcs")
         (vcs/init)))
   (vcs-add-if-not "sitedef.clj")
   ; create any missing directories
   (doseq [dir [(file-str (:site-index-dir config))
                (file-str (:site-template-dir config))
                (file-str (:site-content-dir config))
                (file-str (:site-root config))
                (file-str (:site-root config) "/static/css")
                (file-str (:site-root config) "/static/js")
                (file-str (:site-root config) "/entries")]]
     (do (debug-fmt "checking dir: %s" dir)
       (condp #(and %1 %2) (.exists dir)
         (.isDirectory dir)
           (do
             (debug-fmt "directory exists: %s" dir) nil)
         (.isFile dir)
           (throw (.IllegalStateException "configured dir is a file"))
         (do
           (debug-fmt "making dir: %s" dir)
           (.mkdirs dir)))))
   (let [css (.getResourceAsStream (clojure.lang.RT/baseLoader)
                           "html/css/site.css")]
     (debug-fmt "got resource: %s"
               (.getResource (clojure.lang.RT/baseLoader)
                             "html/css/site.css"))
     (copy css (file-str (:site-root config) "/static/css/site.css"))
     (vcs-add-if-not (str (:site-root config) "/static/css/site.css"))))

(with-config task-import-wordpress
  [config entries]
  (doseq [entry entries]
    (let [filename (hill-file->disk (:site-content-dir config) entry)]
      (info-fmt "importing: %s" (:title entry))
      ; we add but don't commit the file to vcs
      (vcs-add-if-not filename))))

(with-config task-build-indexes
  [config files]
  (doseq [file files]
     (info-fmt "updating index for [%s]" file)
     (task-update-index file)))

; TODO(jwall): should only build tag pages that were affected by file
(with-config task-build-content
  [site-config changed-files]
  (let [changed-content-files (filter #(is-content-file? site-config %) changed-files)]
    (doseq [changed-file changed-content-files]
      ; TODO(jwall): what about sitedef.clj changes?
      (debug-fmt "processing changed file: [%s]" changed-file)
      (do
        (debug-fmt "processing changed content file: [%s]" changed-file)
        ;   rebuild site with changed files
        (task-build-entry-pages site-config (hill-file<-disk (dirname changed-file)
                                                             (basename changed-file))))))
    ; TODO(jwall): what about template-files?
    ; rebuild the aggregate pages for the site
  (task-build-aggregate-pages site-config))

;; public API
(with-config #^{:doc
"molehill rebuild-indexes
    Rebuild the molehill index files. You can use this to repair an index."
:command true
  } rebuild-indexes
  [site-config]
  (assert (site-inited? site-config))
  (vcs/with-commit "rebuilding indexes"
    (vcs/with-commit "deleting old index files"
                (doseq [index-file (drop 1 (file-seq
                                     (file-str (:site-index-dir site-config))))]
                (info-fmt "deleting: [%s]" index-file)
                (vcs/rm (str index-file))))
    (info "deleted old index files")
    (task-site-init site-config) ; TODO(jwall): Ok this is really hacky
    (task-build-indexes (all-content-files))))

(with-config #^{:doc
"molehill build
    Build a molehill site after changes. Run this command after you've
    finshed editing content or added new content."
:command true
  } build
  [site-config]
   ; load all clojure files in template directory
   (assert (site-inited? site-config))
   ; find changed files
   (vcs/with-commit "Building site"
     (let [files (find-changed-files)]
       (task-build-content files)))
   (rebuild-indexes))

(with-config #^{:doc
"molehill build-entry <path to entry file>
    Force a build of a particular molehill entry file."
:command true
  } build-entry
  [config entry]
  (rebuild-indexes)
  (vcs/with-commit (str "building entry: " entry)
    (assert (site-inited? config))
    (task-build-entry-pages config (hill-file<-disk (dirname entry)
                                                    (basename entry)))))

(with-config #^{:doc
"molehill rebuild
    Force a complete site rebuild. You can use this if you need
    the site to rebuild with new templates or if you have lost
    some of the generated content."
:command true
  } rebuild
  [site-config]
   (rebuild-indexes)
   (vcs/with-commit "rebuilding site"
    (doseq [content-file (file-seq (file-str (:site-content-dir site-config)))
            :when (and (.isFile content-file)
                       (= (seq ".html") (take-last 5 (.getName content-file))))]
      (debug-fmt "deleting: [%s]" content-file)
      (vcs/rm (str content-file)))
     (assert (site-inited? site-config))
     (task-build-content (all-content-files))))

(with-config #^{:doc
"molehill build-aggregates
    Build the aggregate pages in a molehill."
:command true
  } build-aggregates
  [config]
   (rebuild-indexes)
   (vcs/with-commit "built aggregate pages"
     (assert (site-inited? config))
     (task-build-aggregate-pages config)))

(with-config #^{:doc
"molehill init
    Initializes a molehill repository. Assumes a sitedef.clj file is present
    in the working directory."
:command true
  } init
  [config]
  (vcs/with-commit "initializing site"
    (info-fmt "starting init task")
    (if (not (site-inited? config))
      (do
        (debug-fmt "site not initialized")
        (task-site-init config)))))

(with-config #^{:doc
"molehill import-wordpress <backup xml file>
    import a wordpress backup file into a molehill"
:command true
  } import-wordpress
  [config rss-file]
  (vcs/with-commit (str "importing wordpress file: " rss-file)
    (assert (site-inited? config))
    (let [rss-entries (iw/process-rss rss-file)]
      (debug-fmt "got my rss entries")
      (debug-fmt "entry count %s" (count rss-entries))
      (task-import-wordpress config rss-entries)
      (build config))))

(with-config #^{:doc
"molehill ls-files
    list all the files molehill tracks"
:command true
  } ls-files
  [config]
   (doseq [file (vcs/all-tracked)]
     (println file)))

(defn #^{:doc
"molehill view-entry
   view a molehill entry in it's parsed and then unparsed form"
:command true
  } view-entry
  [entry]
  (do
    (println ((:parsed-content (hill-file<-disk entry))))
    (println (:content (hill-file<-disk entry)))))

(with-config #^{:doc
"molehill ls-content
    list molehill entries"
:command true
  } ls-content
  [config]
   (doseq [file (all-content-files)]
     (println file)))

(with-config #^{:doc
"molehill ls-tags
    list molehill tags with statistics on them"
:command true
  } ls-tags
  [config]
   (let [tags (get-all-tags)]
     (doseq [[cnt tag size] (map-tag-size tags)]
       (println (format "%s: %s - size: %s" tag cnt size)))))

(with-config #^{:doc
"molehill ls-tag-entries <tag>
    list molehill entries tagged by <tag>"
:command true
  } ls-tag-entries
  [config tag]
   (assert (not (nil? tag)))
   (let [files (get-all-for-tag tag)]
     (doseq [file files]
       (println (format "%s" (hill-file-slug file))))))

(with-config #^{:doc
"molehill debug-test-templates
    attempts to load templates"
:command true
  } debug-test-templates
  [config]
   (load-templates))

(with-config do-test-site
  [config port]
    (let [doc-root (file-str (:site-root config))]
      (println (str "site-root " (:site-root config)))
      (println (str "DocRoot " (.getAbsolutePath doc-root)))
      (println (str "Open your browser to http://localhost:" port))
      (println "Ctrl-c to end")
      (hs/http-start (Integer/valueOf port) (str (.getAbsolutePath doc-root) "/"))))

(defn #^{:doc
"molehill test-site [port]
   runs a small test server to view the molehill in"
:command true
  } test-site
  ([] (do-test-site 9001))
  ([port] (do-test-site port)))

;TODO(jwall): push command
;TODO(jwall): new command
(defn resolve-cmd
  [cmd]
  (ns-resolve 'com.marzhillstudios.molehill.site (symbol cmd)))

(declare app-help)

(defn get-help
  ([cmd]
     (:doc (meta (resolve-cmd cmd)))))

(defn #^{:doc
"molehill help [cmd]
    Displays help text. Takes an optional command"
:command true
  } help
  ([]
     (println app-help))
  ([cmd msg]
     (println msg)
     (println (get-help cmd)))
  ([cmd]
     (println (get-help cmd))))

(defn #^{:doc
"molehill new
     Creates a new molehill site. Creates a skeleton sitedef.clj file
     as well as running init to get up and running quickly."
:command true
  } new
    ([]
       (let [site-name (prompt "site name?")
             base-url (prompt "domain name?")
             root (prompt "site root")
             new-conf '(:site-name site-name
                        :site-base-url base-url
                        :site-root site-root
                        :site-template-dir "_tmpl"
                        :site-content-dir "_content"
                        :index :sample-index-tmpl
                        :feed :sample-feed-tmpl
                        :entry :sample-entry-tmpl
                        :tag :sample-tag-tmpl
                        :tag-landing :sample-tag-landing-tmpl)]
         ; write out default config
         ; TODO(jwall): pretty print would be nice
         (with-out-writer "sitedef.clj"
           (prn (cons 'defsite (seq new-conf))))
         ; run init site
         ; write out sample templates
         )
       nil))

(def app-help
(str "Molehill static site generator and content management tool.

General Useage: 
"
 (get-help "init") "\n\n"
 (get-help "new-entry") "\n\n"
 (get-help "new")
"
Build commands:

" (get-help "build") "\n\n"
  (get-help "rebuild") "\n\n"
  (get-help "build-aggregates") "\n\n"
  (get-help "build-entry") "\n\n"
  (get-help "rebuild-indexes") "\n\n"

"
Query commands:

" (get-help "ls-content") "\n\n"
  (get-help "ls-tags") "\n\n"
  (get-help "ls-tag-entries") "\n\n"
  (get-help "ls-files") "\n\n"

"
Misc:

" (get-help "import-wordpress") "\n\n"
  (get-help "view-entry") "\n\n"
  (get-help "test-site") "\n\n"
  (get-help "repl")
))

(with-config #^{:doc
"molehill new-entry \"Article Title\"
    Writes a new hill file to disk with some initial values.
    Takes the article title as an argument. Don't forget the quotes."
:command true
  } new-entry
  [config entry]
  (let [new-file (hill-file->disk
                   (:site-content-dir config)
                   (new-hill-file entry))]
    (vcs-add-if-not new-file)
    (println new-file)
    (flush)))

(with-config #^{:doc
"molehill repl
    Runs a repl where you can interact with the molehill code."
:command true
  } repl
  [config]
  (cm/main))

(defn -main
  ([] (help))
  ([cmd & opts]
    (try
      (let [cmdfn (resolve-cmd cmd)]
        (cond
          (:command (meta cmdfn))
            (try
                (apply cmdfn opts)
                (catch Exception e (help cmd (.printStackTrace e))))
           :else (help)))
      (catch Exception e
        (help)))))
